// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/gdax/api_order/native_private_client.h"

#include <list>
#include <memory>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/gdax/api_util/transfers_account_store.h"
#include "coin2/exchange/gdax/symbology/product.h"

using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;
using coin2::exchange::gdax::api_order::GdaxTransfersAccountStore;

auto GdaxNativePrivateClient::GetRestOrderParams(const OrderSpec& order_spec, int64_t proc_order_id)
    -> RestParams {
  auto params = RestParams();

  using coin2::exchange::gdax::symbology::GdaxProduct;
  auto product = GdaxProduct::FromStr(order_spec.product, order_spec.order_created_time);
  auto product_holder = GetProductHolder(product, order_spec.order_created_time);
  auto formatter = product_holder->product_info().order_formatter();
  params.set_product_id(order_spec.product);
  params.set_size(formatter.FormatQty(order_spec.qty));
  params.set_client_oid("193a34c1ad96b" + std::to_string(proc_order_id));
  // self trade prevention: cancel newest
  params.set_stp("cn");

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("market");
  } else {
    params.set_type("limit");
    params.set_price(formatter.FormatPrice(order_spec.price));

    if (order_spec.post_only) {
      params.set_post_only("true");
    }

    if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
      params.set_time_in_force("FOK");
    } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
      params.set_time_in_force("IOC");
    } else {
      params.set_time_in_force("GTC");
    }
  }
  return params;
}

auto GdaxNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  const auto& order_spec = order_context.order_spec();
  auto params = RestParams();
  params.set_product_id(order_spec.product);

  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id());
  } else {
    auto tmp_str = std::to_string(order_context.proc_order_id());
    tmp_str.insert(7, "-");
    tmp_str.insert(3, "-");
    params.set_client_oid("193a34c1-ad96-b" + tmp_str);
  }
  return params;
}

GdaxNativePrivateClient::GdaxNativePrivateClient(const NativeClientConfig& config_override) {
  NativeClientConfig config;
  config.set_rest_host("api.pro.coinbase.com");
  config.set_rest_host_port(443);
  config.MergeFrom(config_override);
  LOG(INFO) << "[GdaxNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<GdaxRestAuth, NativeClientConfig>(config);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::QueryAccountBalance() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/accounts", &builder);

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::QueryOrders(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutGet("product_id", params.product_id());
  builder.PutGet("status", params.status());
  builder.PutGet("limit", params.limit());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/orders", &builder);

  if (params.status() == "all") {
    context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  } else {
    context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  }
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::QueryTrades(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutGet("product_id", params.product_id());
  builder.PutGet("limit", params.limit());
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/fills", &builder);

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::TransferInit() {
  UrlParamsBuilder builder;
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/accounts", &builder);

  context->query_type = GdaxOrderSubTopicId::REST_TRANSFERS_INIT;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::QueryTransfer(const RestParams& params) {
  UrlParamsBuilder builder;
  if (!params.has_currency_id()) {
    return {};
  }

  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("start_date", params.start_time());
    builder.PutGet("end_date", params.end_time());
  }
  builder.PutGet("limit", 500);

  auto path = absl::StrCat("/accounts/", params.currency_id(), "/ledger");
  auto* context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  context->query_type = OrderSubTopicId::TRANSFER;
  context->extra_info = params.currency_id();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::QueryDeposit(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutGet("type", "deposit");
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("start_date", params.start_time());
    builder.PutGet("end_date", params.end_time());
  }

  builder.PutGet("limit", 1000);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/transfers", &builder);

  context->query_type = GdaxOrderSubTopicId::REST_DEPOSIT;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::QueryWithdrawal(
    const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutGet("type", "withdraw");
  if (params.has_start_time() && params.has_end_time()) {
    builder.PutGet("start_date", params.start_time());
    builder.PutGet("end_date", params.end_time());
  }
  builder.PutGet("limit", 1000);
  auto* context = GetAuth()->CreateRequestByGetWithSignature("/transfers", &builder);

  context->query_type = GdaxOrderSubTopicId::REST_WITHDRAWAL;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::PlaceOrder(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutPost("product_id", params.product_id());
  builder.PutPost("side", params.side());
  builder.PutPost("type", params.type());
  builder.PutPost("time_in_force", params.time_in_force());
  builder.PutPost("price", params.price());
  builder.PutPost("size", params.size());
  builder.PutPost("client_oid", params.client_oid());
  builder.PutPost("stp", params.stp());
  builder.PutPost("post_only", params.post_only());
  auto* context = GetAuth()->CreateRequestByPostWithSignature("/orders", &builder);

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::CancelOrder(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutPost("product_id", params.product_id());
  CHECK(params.has_order_id() != params.has_client_oid())
      << "only one of order_id and client_oid can be set!" << params.DebugString();

  if (params.has_order_id()) {
    auto* context =
        GetAuth()->CreateRequestByDeleteWithSignature("/orders/" + params.order_id(), &builder);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  } else {
    auto* context = GetAuth()->CreateRequestByDeleteWithSignature(
        "/orders/client:" + params.client_oid(),
        &builder);

    context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
    return std::shared_ptr<AsioHttpContext>(context);
  }
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::CancelAllOrder(const RestParams& params) {
  UrlParamsBuilder builder;
  builder.PutPost("product_id", params.product_id());
  auto* context = GetAuth()->CreateRequestByDeleteWithSignature("/orders", &builder);

  context->query_type = OrderSubTopicId::MULTI_CANCEL_RESPONSE;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  switch (order.type()) {
    case OrderExecutionProto::SUBMIT_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return PlaceOrder(params);
    }
    case OrderExecutionProto::CANCEL_ORDER: {
      auto order_spec = OrderSpec::FromProto(order.order_spec());
      auto params = GetRestOrderParams(*order_spec, order.proc_order_id());
      return CancelOrder(params);
    }
    default: {
      return nullptr;
    }
  }
}

std::shared_ptr<AsioHttpContext> GdaxNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return QueryAccountBalance();
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      params.set_status("open");
      return QueryOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}

AsioHttpContextList GdaxNativePrivateClient::SubmitRestQueryList(const RestQueryProto& query) {
  AsioHttpContextList list;
  RestParams params;

  auto fill_func = [&params](int64_t start_time, int64_t end_time) {
    auto iso_start_time = Iso8601FromTimestamp(start_time);
    auto iso_end_time = Iso8601FromTimestamp(end_time);
    params.set_start_time(iso_start_time);
    params.set_end_time(iso_end_time);
  };

  switch (query.type()) {
    case RestQueryProto::INIT_QUERY_INFO: {
      list.emplace_back(TransferInit());
      break;
    }
    case RestQueryProto::QUERY_TRANSFERS: {
      fill_func(query.start_time(), query.end_time());
      params.set_currency_id(query.gdax_currency_id());
      list.emplace_back(QueryTransfer(params));
      break;
    }

    case RestQueryProto::QUERY_DEPOSIT: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryDeposit(params));
      break;
    }
    case RestQueryProto::QUERY_WITHDRAW: {
      fill_func(query.start_time(), query.end_time());
      list.emplace_back(QueryWithdrawal(params));
      break;
    }

    default: {
      return list;
    }
  }

  return list;
}